/**
 * RetryOnExceptionStrategyInterceptor.java
 * 
 * Created on Jun 4, 2008 at 8:14:09 PM
 */
package org.lego4j;

import static java.lang.String.format;

import java.util.ArrayList;
import java.util.List;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.lego4j.policy.RetryMaxNumberOfTimes;

/**
 * AOP Alliance MethodInterceptor that provides a retry on exception strategy 
 * based on a given instance of {@link IRetryOnExceptionPolicy}.
 *
 * @author <a href="alexguev@gmail.com">Alexei Guevara</>
 * @version $Revision:$ updated on $Date:$ by $Author:$
 */
public class RetryOnExceptionPolicyInterceptor
    extends AbstractLego4jMethodInterceptor<IRetryOnExceptionPolicy, RetryOnException>
    implements MethodInterceptor {
    
    public RetryOnExceptionPolicyInterceptor() {
        super( RetryOnException.class );
    }

    private static final Log ourLog = LogFactory.getLog( RetryOnExceptionPolicyInterceptor.class );

    public Object invoke(MethodInvocation theInvocation) throws Throwable {
        int retries = 0;
        boolean isExRecoverable;
        long startTime = System.currentTimeMillis();
        
        Throwable exThrown;
        
        do {
            isExRecoverable = false;
            try {
                return theInvocation.proceed();
            }
            catch (Throwable e) {
                exThrown = e;
                
                IRetryOnExceptionPolicy policy = 
                    createUniquePolicyForMethod( theInvocation.getMethod() );
                
                isExRecoverable = policy.isExceptionRecoverable(e);
                if (isExRecoverable) {
                    long delay = policy.nextDelay( startTime, retries );
                    if (delay < 0) {
                        break;
                    }
                    else {
                        boolean interruptedWhileSleeping = doSleep( delay );
                        if (interruptedWhileSleeping) {
                            //if interrupted while waiting to retry then break, so the original exception is thrown!
                            //the rationale is that if the thread waiting to retry is interrupted, then
                            //we can assume the user is not interested in retrying.
                            ourLog.debug( format("Interrupted while waiting to retry method %s", theInvocation.getMethod() ) );
                            break;
                        }
                    }
                }
            }
        } while ( isExRecoverable ==  true );
        
        throw exThrown;
    }

    private boolean doSleep( long theDelay ) {
        try {
            Thread.sleep( theDelay );
            return false;
        } 
        catch (InterruptedException _e) {
            return true;
        }
    }

    @Override
    protected IRetryOnExceptionPolicy createPolicy( RetryOnException annotation) {
        return new RetryMaxNumberOfTimes(
        		exceptionsInAnnotation(annotation), 
        		annotation.retries(), 
        		annotation.delay() );
    }

	private List<Class<? extends Exception>> exceptionsInAnnotation(
			RetryOnException theAnnotation) {
		List<Class<? extends Exception>> exceptions = new ArrayList<Class<? extends Exception>>();
        for (Class<? extends Exception> exClazz : theAnnotation.e()) {
        	exceptions.add(exClazz);	
		}
		return exceptions;
	}
    
}
